Micron Document
NexusPi Git Node

Commit efb87e1bd3a7ba19f45652b4f80a4ade1bb692a5


Parents : a326476
Author : James L <jrl290@gmail.com>
Date : 2026-06-01T21:38:48-04:00

Boundary mode: 4 backbones + LoRa local trust fix

Changes

5 files changed, 295 insertions(+), 137 deletions(-)

M Display.h +30 -14
M FirewallConfig.h +64 -37
M FirewallMode.h +107 -41

Diff

diff --git a/Display.h b/Display.h
index 1f9f8af..371ca4f 100755
--- a/Display.h
+++ b/Display.h
@@ -806,18 +806,20 @@ void draw_stat_area() {
}
// Row 3 — WAN / backbone TCP
- if (firewall_state.tcp_mode == 0) {
+ size_t wan_enabled = firewall_backbone_enabled_count();
+ size_t wan_connected = firewall_backbone_connected_count();
+ if (wan_enabled == 0) {
stat_area.drawCircle(4, 23, 3, SSD1306_WHITE);
stat_area.setCursor(10, 26);
- stat_area.print("wan");
- } else if (firewall_state.tcp_connected) {
+ stat_area.printf("WAN:0/4");
+ } else if (wan_connected > 0) {
stat_area.fillCircle(4, 23, 3, SSD1306_WHITE);
stat_area.setCursor(10, 26);
- stat_area.print("WAN");
+ stat_area.printf("WAN:%u/4", (unsigned)wan_connected);
} else {
stat_area.drawCircle(4, 23, 3, SSD1306_WHITE);
stat_area.setCursor(10, 26);
- stat_area.print("wan");
+ stat_area.printf("WAN:%u/4", (unsigned)wan_connected);
}
// Row 4 — LAN / local TCP server (hidden when disabled)
@@ -904,7 +906,15 @@ void draw_disp_area() {
p_by = 18;
disp_area.fillRect(0, 0, disp_area.width(), disp_area.height(), SSD1306_BLACK);
}
- if (!device_init_done) disp_area.drawBitmap(0, p_by, bm_boot, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ if (!device_init_done) {
+ disp_area.drawBitmap(0, p_by, bm_boot, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ disp_area.setFont(SMALL_FONT);
+ disp_area.setTextWrap(false);
+ disp_area.setTextColor(SSD1306_WHITE);
+ disp_area.setTextSize(1);
+ disp_area.setCursor(2, 8);
+ disp_area.printf("v%u.%u", (unsigned)MAJ_VERS, (unsigned)MIN_VERS);
+ }
if (firmware_update_mode) disp_area.drawBitmap(0, p_by, bm_fw_update, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
} else {
if (!disp_ext_fb or bt_ssp_pin != 0) {
@@ -928,23 +938,28 @@ void draw_disp_area() {
disp_area.setTextColor(SSD1306_WHITE);
// Radio info
- disp_area.setCursor(2, 18);
+ disp_area.setCursor(3, 16);
if (radio_online) {
- disp_area.printf("%.3fMHz", (float)lora_freq / 1000000.0);
+ disp_area.printf("%.3f MHz", (float)lora_freq / 1000000.0);
} else {
disp_area.print("Radio OFF");
}
- disp_area.setCursor(2, 29);
+ disp_area.setCursor(3, 25);
+ if (radio_online) {
+ disp_area.printf("SF:%d %.0fk", lora_sf, (float)lora_bw / 1000.0);
+ }
+
+ disp_area.setCursor(3, 34);
if (radio_online) {
- disp_area.printf("SF%d %.0fk", lora_sf, (float)lora_bw / 1000.0);
+ disp_area.printf("CR:4/%d", lora_cr);
}
- // 1px separator after SF line
- disp_area.drawLine(0, 34, disp_area.width()-1, 34, SSD1306_WHITE);
+ // 1px separator after radio info
+ disp_area.drawLine(0, 40, disp_area.width()-1, 40, SSD1306_WHITE);
// IP address
- disp_area.setCursor(2, 44);
+ disp_area.setCursor(3, 49);
if (firewall_state.wifi_connected) {
disp_area.print(wr_device_ip);
} else {
@@ -957,7 +972,8 @@ void draw_disp_area() {
disp_area.printf("Port:%u", firewall_state.ap_tcp_port);
}
- disp_area.drawLine(0, 60, disp_area.width()-1, 60, SSD1306_WHITE);
+ // Bottom divider (maps to y=127 at 2x display scale)
+ disp_area.drawLine(0, 63, disp_area.width()-1, 63, SSD1306_WHITE);
#else
if (radio_online && display_diagnostics) {
#ifdef HAS_RNS

diff --git a/FirewallConfig.h b/FirewallConfig.h
index 8bf5199..0023ef4 100755
--- a/FirewallConfig.h
+++ b/FirewallConfig.h
@@ -219,27 +219,36 @@ static void config_send_html() {
// ── TCP Backbone Section ──
html += F(
- "<h2>&#x1f310; TCP Backbone</h2>"
- "<label>Mode</label>"
- "<select name='tcp_mode'>"
+ "<h2>&#x1f310; TCP Backbones</h2>"
+ "<p class='note'>Configure up to four independent TCP backbone uplinks. "
+ "Each enabled slot becomes its own boundary interface. Leave host blank to disable a slot.</p>"
);
- html += F("<option value='0'");
- if (firewall_state.tcp_mode == 0) html += F(" selected");
- html += F(">Disabled</option>");
- html += F("<option value='1'");
- if (firewall_state.tcp_mode == 1) html += F(" selected");
- html += F(">Client (connect to backbone)</option>");
- html += F("</select>");
-
- html += F("<label>Backbone Host</label>");
- html += F("<input name='bb_host' maxlength='63' placeholder='e.g. 192.168.1.100' value='");
- html += String(firewall_state.backbone_host);
- html += F("'>");
-
- html += F("<label>Backbone Port</label>");
- html += F("<input name='bb_port' type='number' min='1' max='65535' value='");
- html += String(firewall_state.backbone_port);
- html += F("'>");
+ for (size_t slot = 0; slot < FIREWALL_BACKBONE_SLOTS; slot++) {
+ html += F("<div class='card'>");
+ html += F("<label>");
+ html += String("Backbone ") + String((int)slot + 1);
+ html += F("</label>");
+
+ html += F("<select name='");
+ html += String("bb") + String((int)slot) + F("_en'>");
+ html += F("<option value='0'");
+ if (!firewall_state.backbones[slot].enabled) html += F(" selected");
+ html += F(">Disabled</option>");
+ html += F("<option value='1'");
+ if (firewall_state.backbones[slot].enabled) html += F(" selected");
+ html += F(">Enabled</option></select>");
+
+ html += F("<label>Host</label><input name='");
+ html += String("bb") + String((int)slot) + F("_host' maxlength='63' placeholder='e.g. rns.example.net' value='");
+ html += String(firewall_state.backbones[slot].host);
+ html += F("'>");
+
+ html += F("<label>Port</label><input name='");
+ html += String("bb") + String((int)slot) + F("_port' type='number' min='1' max='65535' value='");
+ html += String(firewall_state.backbones[slot].port);
+ html += F("'>");
+ html += F("</div>");
+ }
// ── Local TCP Server Section ──
html += F(
@@ -560,17 +569,25 @@ static void config_handle_save() {
eeprom_update(eeprom_addr(ADDR_CONF_DROT), (uint8_t)display_rotation);
// ── TCP backbone settings ──
- firewall_state.tcp_mode = (uint8_t)config_server->arg("tcp_mode").toInt(); // 0=disabled, 1=client
- if (firewall_state.tcp_mode > 1) firewall_state.tcp_mode = 0;
- firewall_state.tcp_port = (uint16_t)config_server->arg("tcp_port").toInt();
- if (firewall_state.tcp_port == 0) firewall_state.tcp_port = 4242;
+ for (size_t slot = 0; slot < FIREWALL_BACKBONE_SLOTS; slot++) {
+ String en_arg = String("bb") + String((int)slot) + String("_en");
+ String host_arg_name = String("bb") + String((int)slot) + String("_host");
+ String port_arg_name = String("bb") + String((int)slot) + String("_port");
- String bb_host = config_server->arg("bb_host");
- memset(firewall_state.backbone_host, 0, sizeof(firewall_state.backbone_host));
- strncpy(firewall_state.backbone_host, bb_host.c_str(), sizeof(firewall_state.backbone_host) - 1);
+ firewall_state.backbones[slot].enabled = (config_server->arg(en_arg).toInt() == 1);
- firewall_state.backbone_port = (uint16_t)config_server->arg("bb_port").toInt();
- if (firewall_state.backbone_port == 0) firewall_state.backbone_port = 4242;
+ String bb_host = config_server->arg(host_arg_name);
+ bb_host.trim();
+ memset(firewall_state.backbones[slot].host, 0, sizeof(firewall_state.backbones[slot].host));
+ strncpy(firewall_state.backbones[slot].host, bb_host.c_str(), sizeof(firewall_state.backbones[slot].host) - 1);
+
+ firewall_state.backbones[slot].port = (uint16_t)config_server->arg(port_arg_name).toInt();
+ if (firewall_state.backbones[slot].port == 0) firewall_state.backbones[slot].port = 4242;
+
+ if (firewall_state.backbones[slot].host[0] == '\0') {
+ firewall_state.backbones[slot].enabled = false;
+ }
+ }
// ── Local TCP server settings ──
firewall_state.ap_tcp_enabled = (config_server->arg("ap_tcp_en").toInt() == 1);
@@ -839,14 +856,24 @@ void config_portal_start() {
if (disp_ready) {
// Show config mode on display
stat_area.fillScreen(SSD1306_BLACK);
- stat_area.setCursor(0, 0);
- stat_area.println("CONFIG MODE");
- stat_area.println("");
- stat_area.println("Connect to:");
- stat_area.println(CONFIG_AP_SSID);
- stat_area.println("");
- stat_area.println("Open browser");
- stat_area.println("http://10.0.0.1");
+ stat_area.setTextColor(SSD1306_WHITE);
+ stat_area.setTextSize(1);
+ stat_area.setTextWrap(false);
+
+ stat_area.setCursor((64 - (6 * 6)) / 2, 0);
+ stat_area.print("CONFIG");
+ stat_area.setCursor((64 - (4 * 6)) / 2, 8);
+ stat_area.print("MODE");
+
+ stat_area.setCursor((64 - (10 * 6)) / 2, 24);
+ stat_area.print("Connect to");
+ stat_area.setCursor((64 - (10 * 6)) / 2, 32);
+ stat_area.print("Wifi SSID:");
+ stat_area.setCursor((64 - (7 * 6)) / 2, 40);
+ stat_area.print("RTNode-");
+ stat_area.setCursor((64 - (5 * 6)) / 2, 48);
+ stat_area.print("Setup");
+
display.clearDisplay();
display.drawBitmap(0, 0, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
display.display();

diff --git a/FirewallMode.h b/FirewallMode.h
index 90cd29d..a6b3556 100755
--- a/FirewallMode.h
+++ b/FirewallMode.h
@@ -21,10 +21,11 @@
// - WAN→LAN traffic is filtered unless a LAN device has "punched a hole"
// by referencing the source or a LAN device has been referenced first.
//
-// The boundary node operates with TWO RNS interfaces:
+// The boundary node operates with a LoRa mesh interface plus up to four
+// TCP backbone client interfaces:
//
// 1. LoRaInterface (MODE_GATEWAY) — radio side, handles LoRa mesh
-// 2. BackboneInterface (MODE_BOUNDARY) — WiFi side, connects to TCP backbone
+// 2. BackboneInterface[1-4] (MODE_BOUNDARY) — WiFi side TCP uplinks
//
// RNS Transport is ALWAYS enabled in firewall mode.
// Packets received on either interface are routed through Transport
@@ -53,6 +54,10 @@
#define FIREWALL_TCP_PORT 4242
#endif
+#define FIREWALL_BACKBONE_SLOTS 4
+#define FIREWALL_BACKBONE_HOST_LEN 64
+#define FIREWALL_BACKBONE_SLOT_BYTES (1 + FIREWALL_BACKBONE_HOST_LEN + 2)
+
// ─── EEPROM Extension Addresses ──────────────────────────────────────────────
// We use the CONFIG area (config_addr) for additional firewall mode settings.
// These are after the existing WiFi SSID/PSK/IP/NM fields.
@@ -100,8 +105,13 @@
#define ADDR_CONF_LT_AL 0x150 // Long-term airtime limit (1 byte, percent * 10)
#define ADDR_CONF_MDNS_EN 0x151 // mDNS enable flag (1 byte; 0x73 = enabled, 0xFF = unset/default-enabled)
#define ADDR_CONF_MDNS_NAME 0x152 // Custom mDNS hostname (33 bytes, null-terminated; empty = auto)
-// Total: 0x173 (371 bytes — extends beyond 256-byte CONFIG area into
-// unused EEPROM gap; safe on ESP32 where EEPROM starts at 824)
+// Extra backbone slots 1-3 (slot 0 remains in the legacy BTCP/BHOST/BHPORT
+// fields for backward compatibility with existing devices).
+#define ADDR_CONF_BSLOT_BASE 0x173
+#define ADDR_CONF_BSLOT_EN(slot) (ADDR_CONF_BSLOT_BASE + ((slot) - 1) * FIREWALL_BACKBONE_SLOT_BYTES)
+#define ADDR_CONF_BSLOT_HOST(slot) (ADDR_CONF_BSLOT_EN(slot) + 1)
+#define ADDR_CONF_BSLOT_PORT(slot) (ADDR_CONF_BSLOT_HOST(slot) + FIREWALL_BACKBONE_HOST_LEN)
+// Total: 0x23C (572 bytes — still within the extended CONFIG area used on ESP32)
#define FIREWALL_ENABLE_BYTE 0x73
#define FIREWALL_APP_MARKER0 0x52
@@ -111,13 +121,17 @@
// ─── Firewall Mode Runtime State ─────────────────────────────────────────────
#ifndef FIREWALL_STATE_DEFINED
#define FIREWALL_STATE_DEFINED
+struct FirewallBackboneSlot {
+ bool enabled;
+ char host[FIREWALL_BACKBONE_HOST_LEN];
+ uint16_t port;
+ bool connected;
+};
+
struct FirewallState {
bool enabled;
bool wifi_enabled; // false = LoRa-only repeater (no WiFi)
- uint8_t tcp_mode; // 0=disabled, 1=client
- uint16_t tcp_port; // Local port (client outbound)
- char backbone_host[64];
- uint16_t backbone_port; // Target port for client mode
+ FirewallBackboneSlot backbones[FIREWALL_BACKBONE_SLOTS];
// AP TCP server settings
bool ap_tcp_enabled; // Whether to run a WiFi AP with TCP server
@@ -154,7 +168,6 @@ struct FirewallState {
// Runtime state
bool wifi_connected;
- bool tcp_connected; // Backbone (WAN) connected
bool ap_tcp_connected; // Local TCP server (LAN) has client
bool ap_active;
uint32_t packets_bridged_lora_to_tcp;
@@ -166,6 +179,26 @@ struct FirewallState {
// Global boundary state instance (defined in RNode_Firmware.ino)
extern FirewallState firewall_state;
+inline size_t firewall_backbone_enabled_count() {
+ size_t count = 0;
+ for (size_t i = 0; i < FIREWALL_BACKBONE_SLOTS; i++) {
+ if (firewall_state.backbones[i].enabled) count++;
+ }
+ return count;
+}
+
+inline size_t firewall_backbone_connected_count() {
+ size_t count = 0;
+ for (size_t i = 0; i < FIREWALL_BACKBONE_SLOTS; i++) {
+ if (firewall_state.backbones[i].enabled && firewall_state.backbones[i].connected) count++;
+ }
+ return count;
+}
+
+inline bool firewall_any_backbone_enabled() {
+ return firewall_backbone_enabled_count() > 0;
+}
+
// ─── Firewall Mode EEPROM Load/Save ─────────────────────────────────────────
inline bool firewall_app_marker_valid() {
@@ -220,12 +253,17 @@ inline void firewall_load_config() {
if (!firewall_state.enabled) {
// Use compile-time defaults
firewall_state.wifi_enabled = true;
- firewall_state.tcp_mode = FIREWALL_TCP_MODE;
- firewall_state.tcp_port = FIREWALL_TCP_PORT;
- strncpy(firewall_state.backbone_host, FIREWALL_BACKBONE_HOST,
- sizeof(firewall_state.backbone_host) - 1);
- firewall_state.backbone_host[sizeof(firewall_state.backbone_host) - 1] = '\0';
- firewall_state.backbone_port = FIREWALL_BACKBONE_PORT;
+ for (size_t i = 0; i < FIREWALL_BACKBONE_SLOTS; i++) {
+ firewall_state.backbones[i].enabled = false;
+ firewall_state.backbones[i].host[0] = '\0';
+ firewall_state.backbones[i].port = FIREWALL_BACKBONE_PORT;
+ firewall_state.backbones[i].connected = false;
+ }
+ firewall_state.backbones[0].enabled = (FIREWALL_TCP_MODE == 1);
+ strncpy(firewall_state.backbones[0].host, FIREWALL_BACKBONE_HOST,
+ sizeof(firewall_state.backbones[0].host) - 1);
+ firewall_state.backbones[0].host[sizeof(firewall_state.backbones[0].host) - 1] = '\0';
+ firewall_state.backbones[0].port = FIREWALL_BACKBONE_PORT;
firewall_state.ap_tcp_enabled = false;
firewall_state.ap_tcp_port = 4242;
firewall_state.ap_ssid[0] = '\0';
@@ -254,29 +292,42 @@ inline void firewall_load_config() {
firewall_state.wifi_enabled = (wifi_en_byte == FIREWALL_ENABLE_BYTE || wifi_en_byte == 0xFF);
// Load from EEPROM
- firewall_state.tcp_mode = EEPROM.read(config_addr(ADDR_CONF_BTCP_MODE));
- if (firewall_state.tcp_mode > 1) firewall_state.tcp_mode = 0; // 0=disabled, 1=client
-
- firewall_state.tcp_port =
- ((uint16_t)EEPROM.read(config_addr(ADDR_CONF_BTCP_PORT)) << 8) |
- (uint16_t)EEPROM.read(config_addr(ADDR_CONF_BTCP_PORT + 1));
- if (firewall_state.tcp_port == 0 || firewall_state.tcp_port == 0xFFFF) {
- firewall_state.tcp_port = FIREWALL_TCP_PORT;
- }
-
- for (int i = 0; i < 63; i++) {
- firewall_state.backbone_host[i] = EEPROM.read(config_addr(ADDR_CONF_BHOST + i));
- if (firewall_state.backbone_host[i] == 0xFF) {
- firewall_state.backbone_host[i] = '\0';
+ firewall_state.backbones[0].enabled =
+ (EEPROM.read(config_addr(ADDR_CONF_BTCP_MODE)) == 1);
+ for (int i = 0; i < (int)sizeof(firewall_state.backbones[0].host) - 1; i++) {
+ firewall_state.backbones[0].host[i] = EEPROM.read(config_addr(ADDR_CONF_BHOST + i));
+ if (firewall_state.backbones[0].host[i] == (char)0xFF) {
+ firewall_state.backbones[0].host[i] = '\0';
}
}
- firewall_state.backbone_host[63] = '\0';
+ firewall_state.backbones[0].host[sizeof(firewall_state.backbones[0].host) - 1] = '\0';
- firewall_state.backbone_port =
+ firewall_state.backbones[0].port =
((uint16_t)EEPROM.read(config_addr(ADDR_CONF_BHPORT)) << 8) |
(uint16_t)EEPROM.read(config_addr(ADDR_CONF_BHPORT + 1));
- if (firewall_state.backbone_port == 0 || firewall_state.backbone_port == 0xFFFF) {
- firewall_state.backbone_port = FIREWALL_BACKBONE_PORT;
+ if (firewall_state.backbones[0].port == 0 || firewall_state.backbones[0].port == 0xFFFF) {
+ firewall_state.backbones[0].port = FIREWALL_BACKBONE_PORT;
+ }
+ firewall_state.backbones[0].connected = false;
+
+ for (size_t slot = 1; slot < FIREWALL_BACKBONE_SLOTS; slot++) {
+ firewall_state.backbones[slot].enabled =
+ (EEPROM.read(config_addr(ADDR_CONF_BSLOT_EN(slot))) == FIREWALL_ENABLE_BYTE);
+ for (int i = 0; i < (int)sizeof(firewall_state.backbones[slot].host) - 1; i++) {
+ firewall_state.backbones[slot].host[i] = EEPROM.read(config_addr(ADDR_CONF_BSLOT_HOST(slot) + i));
+ if (firewall_state.backbones[slot].host[i] == (char)0xFF) {
+ firewall_state.backbones[slot].host[i] = '\0';
+ }
+ }
+ firewall_state.backbones[slot].host[sizeof(firewall_state.backbones[slot].host) - 1] = '\0';
+
+ firewall_state.backbones[slot].port =
+ ((uint16_t)EEPROM.read(config_addr(ADDR_CONF_BSLOT_PORT(slot))) << 8) |
+ (uint16_t)EEPROM.read(config_addr(ADDR_CONF_BSLOT_PORT(slot) + 1));
+ if (firewall_state.backbones[slot].port == 0 || firewall_state.backbones[slot].port == 0xFFFF) {
+ firewall_state.backbones[slot].port = FIREWALL_BACKBONE_PORT;
+ }
+ firewall_state.backbones[slot].connected = false;
}
// Load AP TCP server settings
@@ -384,7 +435,9 @@ inline void firewall_load_config() {
firewall_state.packets_bridged_tcp_to_lora = 0;
firewall_state.last_bridge_activity = 0;
firewall_state.wifi_connected = false;
- firewall_state.tcp_connected = false;
+ for (size_t i = 0; i < FIREWALL_BACKBONE_SLOTS; i++) {
+ firewall_state.backbones[i].connected = false;
+ }
firewall_state.ap_active = false;
}
@@ -392,15 +445,28 @@ inline void firewall_save_config() {
EEPROM.write(config_addr(ADDR_CONF_BMODE), FIREWALL_ENABLE_BYTE);
EEPROM.write(config_addr(ADDR_CONF_WIFI_EN),
firewall_state.wifi_enabled ? FIREWALL_ENABLE_BYTE : 0x00);
- EEPROM.write(config_addr(ADDR_CONF_BTCP_MODE), firewall_state.tcp_mode);
- EEPROM.write(config_addr(ADDR_CONF_BTCP_PORT), (firewall_state.tcp_port >> 8) & 0xFF);
- EEPROM.write(config_addr(ADDR_CONF_BTCP_PORT + 1), firewall_state.tcp_port & 0xFF);
- for (int i = 0; i < 63; i++) {
- EEPROM.write(config_addr(ADDR_CONF_BHOST + i), firewall_state.backbone_host[i]);
+ EEPROM.write(config_addr(ADDR_CONF_BTCP_MODE), firewall_state.backbones[0].enabled ? 1 : 0);
+ // Keep legacy BTCP_PORT in sync with backbone slot 1 for compatibility
+ // with older tooling that still inspects this field.
+ EEPROM.write(config_addr(ADDR_CONF_BTCP_PORT), (firewall_state.backbones[0].port >> 8) & 0xFF);
+ EEPROM.write(config_addr(ADDR_CONF_BTCP_PORT + 1), firewall_state.backbones[0].port & 0xFF);
+ for (int i = 0; i < (int)sizeof(firewall_state.backbones[0].host) - 1; i++) {
+ EEPROM.write(config_addr(ADDR_CONF_BHOST + i), firewall_state.backbones[0].host[i]);
}
EEPROM.write(config_addr(ADDR_CONF_BHOST + 63), 0x00);
- EEPROM.write(config_addr(ADDR_CONF_BHPORT), (firewall_state.backbone_port >> 8) & 0xFF);
- EEPROM.write(config_addr(ADDR_CONF_BHPORT + 1), firewall_state.backbone_port & 0xFF);
+ EEPROM.write(config_addr(ADDR_CONF_BHPORT), (firewall_state.backbones[0].port >> 8) & 0xFF);
+ EEPROM.write(config_addr(ADDR_CONF_BHPORT + 1), firewall_state.backbones[0].port & 0xFF);
+
+ for (size_t slot = 1; slot < FIREWALL_BACKBONE_SLOTS; slot++) {
+ EEPROM.write(config_addr(ADDR_CONF_BSLOT_EN(slot)),
+ firewall_state.backbones[slot].enabled ? FIREWALL_ENABLE_BYTE : 0x00);
+ for (int i = 0; i < (int)sizeof(firewall_state.backbones[slot].host) - 1; i++) {
+ EEPROM.write(config_addr(ADDR_CONF_BSLOT_HOST(slot) + i), firewall_state.backbones[slot].host[i]);
+ }
+ EEPROM.write(config_addr(ADDR_CONF_BSLOT_HOST(slot) + sizeof(firewall_state.backbones[slot].host) - 1), 0x00);
+ EEPROM.write(config_addr(ADDR_CONF_BSLOT_PORT(slot)), (firewall_state.backbones[slot].port >> 8) & 0xFF);
+ EEPROM.write(config_addr(ADDR_CONF_BSLOT_PORT(slot) + 1), firewall_state.backbones[slot].port & 0xFF);
+ }
// AP TCP server settings
EEPROM.write(config_addr(ADDR_CONF_AP_TCP_EN),

diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino
index 7f135a9..b7130db 100755
--- a/RNode_Firmware.ino
+++ b/RNode_Firmware.ino
@@ -263,8 +263,13 @@ RNS::FileSystem filesystem(RNS::Type::NONE);
#ifdef FIREWALL_MODE
// Firewall mode: TCP backbone interface + state
FirewallState firewall_state = {};
-RNS::Interface tcp_rns_interface(RNS::Type::NONE);
-TcpInterface* tcp_interface_ptr = nullptr;
+RNS::Interface tcp_rns_interfaces[FIREWALL_BACKBONE_SLOTS] = {
+ RNS::Interface(RNS::Type::NONE),
+ RNS::Interface(RNS::Type::NONE),
+ RNS::Interface(RNS::Type::NONE),
+ RNS::Interface(RNS::Type::NONE)
+};
+TcpInterface* tcp_interface_ptrs[FIREWALL_BACKBONE_SLOTS] = { nullptr, nullptr, nullptr, nullptr };
// Local TCP server
RNS::Interface local_tcp_rns_interface(RNS::Type::NONE);
TcpInterface* local_tcp_interface_ptr = nullptr;
@@ -861,27 +866,46 @@ void setup() {
HEAD("Firewall Mode: WiFi DISABLED (LoRa-only repeater)", RNS::LOG_TRACE);
}
- // Register TCP backbone interface if enabled (mode 1 = client)
- if (firewall_state.wifi_enabled && firewall_state.tcp_mode == 1) {
- tcp_interface_ptr = new TcpInterface(
- TCP_IF_MODE_CLIENT,
- firewall_state.tcp_port,
- firewall_state.backbone_host,
- firewall_state.backbone_port
- );
- tcp_rns_interface = tcp_interface_ptr;
- tcp_rns_interface.mode(RNS::Type::Interface::MODE_BOUNDARY);
- tcp_rns_interface.is_backbone(true);
- RNS::Transport::register_interface(tcp_rns_interface);
+ // Register configured TCP backbone interfaces.
+ size_t configured_backbones = 0;
+ if (firewall_state.wifi_enabled) {
+ for (size_t slot = 0; slot < FIREWALL_BACKBONE_SLOTS; slot++) {
+ FirewallBackboneSlot& backbone = firewall_state.backbones[slot];
+ if (!backbone.enabled) {
+ continue;
+ }
+ if (backbone.host[0] == '\0') {
+ char _bm_msg[96];
+ snprintf(_bm_msg, sizeof(_bm_msg), "Backbone %u enabled without host; skipping", (unsigned)(slot + 1));
+ HEAD(_bm_msg, RNS::LOG_WARNING);
+ backbone.enabled = false;
+ continue;
+ }
+
+ char iface_name[24];
+ snprintf(iface_name, sizeof(iface_name), "BackboneInterface%u", (unsigned)(slot + 1));
+ tcp_interface_ptrs[slot] = new TcpInterface(
+ TCP_IF_MODE_CLIENT,
+ backbone.port,
+ backbone.host,
+ backbone.port,
+ iface_name
+ );
+ tcp_rns_interfaces[slot] = tcp_interface_ptrs[slot];
+ tcp_rns_interfaces[slot].mode(RNS::Type::Interface::MODE_BOUNDARY);
+ tcp_rns_interfaces[slot].is_backbone(true);
+ RNS::Transport::register_interface(tcp_rns_interfaces[slot]);
+ configured_backbones++;
- {
char _bm_msg[128];
- snprintf(_bm_msg, sizeof(_bm_msg), "TCP backbone: client -> %s:%d",
- firewall_state.backbone_host, firewall_state.backbone_port);
+ snprintf(_bm_msg, sizeof(_bm_msg), "TCP backbone %u: client -> %s:%d",
+ (unsigned)(slot + 1), backbone.host, backbone.port);
HEAD(_bm_msg, RNS::LOG_TRACE);
}
- } else if (firewall_state.tcp_mode == 0) {
- HEAD("Firewall Mode: TCP backbone DISABLED", RNS::LOG_TRACE);
+ }
+
+ if (configured_backbones == 0) {
+ HEAD("Firewall Mode: TCP backbones DISABLED", RNS::LOG_TRACE);
}
// Register local TCP server if enabled
@@ -931,9 +955,13 @@ void setup() {
#ifdef FIREWALL_MODE
// Start TCP interfaces after Reticulum is running
if (firewall_state.wifi_enabled && (wifi_is_connected() || wifi_mode == WR_WIFI_AP)) {
- if (tcp_interface_ptr) {
- tcp_interface_ptr->start();
- HEAD("Firewall Mode: TCP backbone started", RNS::LOG_TRACE);
+ for (size_t slot = 0; slot < FIREWALL_BACKBONE_SLOTS; slot++) {
+ if (tcp_interface_ptrs[slot]) {
+ tcp_interface_ptrs[slot]->start();
+ }
+ }
+ if (firewall_any_backbone_enabled()) {
+ HEAD("Firewall Mode: TCP backbones started", RNS::LOG_TRACE);
}
if (local_tcp_interface_ptr) {
local_tcp_interface_ptr->start();
@@ -995,12 +1023,12 @@ void setup() {
HEAD("LoRa Interface: MODE_GATEWAY", RNS::LOG_TRACE);
{
char _bm_info[128];
- if (firewall_state.tcp_mode == 1) {
- snprintf(_bm_info, sizeof(_bm_info), "TCP Backbone: client -> %s:%d",
- firewall_state.backbone_host, firewall_state.backbone_port);
+ if (firewall_any_backbone_enabled()) {
+ snprintf(_bm_info, sizeof(_bm_info), "TCP Backbones: %u configured",
+ (unsigned)firewall_backbone_enabled_count());
HEAD(_bm_info, RNS::LOG_TRACE);
} else {
- HEAD("TCP Backbone: DISABLED", RNS::LOG_TRACE);
+ HEAD("TCP Backbones: DISABLED", RNS::LOG_TRACE);
}
if (firewall_state.ap_tcp_enabled) {
snprintf(_bm_info, sizeof(_bm_info), "Local TCP Server: port %d (MODE_GATEWAY)",
@@ -2589,22 +2617,31 @@ void loop() {
if (firewall_state.wifi_enabled) {
// Start TCP interfaces if WiFi just connected and not yet started
if (wifi_is_connected()) {
- if (tcp_interface_ptr && !tcp_interface_ptr->isStarted()) {
- tcp_interface_ptr->start();
- Serial.println("[Firewall] WiFi connected, TCP backbone started");
+ bool started_backbone = false;
+ for (size_t slot = 0; slot < FIREWALL_BACKBONE_SLOTS; slot++) {
+ if (tcp_interface_ptrs[slot] && !tcp_interface_ptrs[slot]->isStarted()) {
+ tcp_interface_ptrs[slot]->start();
+ started_backbone = true;
+ }
+ }
+ if (started_backbone) {
+ Serial.println("[Firewall] WiFi connected, TCP backbones started");
}
if (local_tcp_interface_ptr && !local_tcp_interface_ptr->isStarted()) {
local_tcp_interface_ptr->start();
Serial.println("[Firewall] WiFi connected, local TCP server started");
}
}
- if (tcp_interface_ptr) {
- tcp_interface_ptr->loop();
+ for (size_t slot = 0; slot < FIREWALL_BACKBONE_SLOTS; slot++) {
+ firewall_state.backbones[slot].connected = false;
+ if (tcp_interface_ptrs[slot]) {
+ tcp_interface_ptrs[slot]->loop();
+ firewall_state.backbones[slot].connected = tcp_interface_ptrs[slot]->isConnected();
+ }
}
if (local_tcp_interface_ptr) {
local_tcp_interface_ptr->loop();
}
- firewall_state.tcp_connected = (tcp_interface_ptr && tcp_interface_ptr->isConnected());
firewall_state.ap_tcp_connected = (local_tcp_interface_ptr && local_tcp_interface_ptr->isConnected());
firewall_state.wifi_connected = wifi_is_connected();
}

diff --git a/lib/microReticulum/src/Transport.cpp b/lib/microReticulum/src/Transport.cpp
index 3ec7e2f..3cfc317 100755
--- a/lib/microReticulum/src/Transport.cpp
+++ b/lib/microReticulum/src/Transport.cpp
@@ -116,7 +116,12 @@ static bool is_backbone_interface(const Interface& iface) {
#ifdef FIREWALL_MODE
static bool is_boundary_trusted_interface(const Interface& iface) {
- return Transport::is_local_client_interface(iface);
+ // In boundary mode, backbone interfaces are WAN/untrusted ingress.
+ // All non-backbone ingress (LoRa, on-device/local interfaces) is local/trusted.
+ if (is_backbone_interface(iface)) {
+ return false;
+ }
+ return true;
}
static bool is_boundary_untrusted_interface(const Interface& iface) {
@@ -1496,26 +1501,33 @@ static bool is_boundary_address_packet(const Packet& packet) {
{
bool is_untrusted_ingress = is_boundary_untrusted_interface(packet.receiving_interface());
bool is_announce = packet.packet_type() == Type::Packet::ANNOUNCE;
+ bool is_backbone_ingress = is_backbone_interface(packet.receiving_interface());
if (is_untrusted_ingress) {
if (is_announce) {
- // Real Reticulum destinations answer path requests with a
- // PATH_RESPONSE announce using the normal announce packet
- // header shape. Only transport-rebroadcasted responses carry
- // HEADER_2 and our transport identity. Treat any matching
- // PATH_RESPONSE for an outstanding discovery request as solicited.
- bool solicited_path_response = packet.context() == Type::Packet::PATH_RESPONSE
- && _discovery_path_requests.find(packet.destination_hash()) != _discovery_path_requests.end();
-
- if (!solicited_path_response) {
- DEBUG("BOUNDARY: BLOCKED unsolicited backbone announce dest=" + packet.destination_hash().toHex().substr(0,8) + " ctx=" + std::to_string(packet.context()) + " hdr=" + std::to_string(packet.header_type()));
- return;
+ if (is_backbone_ingress) {
+ // Real Reticulum destinations answer path requests with a
+ // PATH_RESPONSE announce using the normal announce packet
+ // header shape. Only transport-rebroadcasted responses carry
+ // HEADER_2 and our transport identity. Treat any matching
+ // PATH_RESPONSE for an outstanding discovery request as solicited.
+ bool solicited_path_response = packet.context() == Type::Packet::PATH_RESPONSE
+ && _discovery_path_requests.find(packet.destination_hash()) != _discovery_path_requests.end();
+
+ if (!solicited_path_response) {
+ DEBUG("BOUNDARY: BLOCKED unsolicited backbone announce dest=" + packet.destination_hash().toHex().substr(0,8) + " ctx=" + std::to_string(packet.context()) + " hdr=" + std::to_string(packet.header_type()));
+ return;
+ }
}
}
// === UNTRUSTED PACKET: gate against all whitelists ===
bool allowed = false;
+ // LoRa/native RF announces must be accepted so path table can form.
+ if (is_announce && !is_backbone_ingress) {
+ allowed = true;
+ }
// Whitelist 1: destination is a local device
- if (_boundary_local_addresses.find(packet.destination_hash()) != _boundary_local_addresses.end()) {
+ else if (_boundary_local_addresses.find(packet.destination_hash()) != _boundary_local_addresses.end()) {
allowed = true;
}
// Whitelist 2: destination was mentioned by a local device

Served by rngit 1.4.2 - Generated in 0.12s